package com.nbstar.film.spider.font.woff;

import java.awt.geom.GeneralPath;
import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.nbstar.film.spider.font.woff.common.ITTFfontBox;
import com.nbstar.film.spider.font.woff.graphics.TTFGraphicsData;
import com.nbstar.film.spider.font.woff.graphics.TTFGraphicsPathData;
import com.nbstar.film.spider.font.woff.table.*;
import com.nbstar.film.spider.font.woff.table.glyf.GlyphData;
import com.nbstar.film.spider.font.woff.table.glyf.GlyphDescription;
import com.nbstar.film.spider.font.woff.table.glyf.GlyphTable;
import com.nbstar.film.spider.font.woff.table.glyf.Point;
import com.nbstar.film.spider.font.woff.table.name.NamingTable;
import com.nbstar.film.spider.font.woff.utils.BoundingBox;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;



/**
 * @ClassName: WoffFont
 * @author: Wang Xindong
 * @date: 2018年8月30日 上午9:34:20
 * @email: wangxindong@travelsky.com
 * @Copyright: Copyright (c) 2018. All rights reserved.
 */
public class WoffFont implements ITTFfontBox, Closeable {

    private static final Logger LOGGER = LoggerFactory.getLogger(WoffFont.class);

    /**
     * woff header
     */
    private WoffHeader header;

    private int numberOfGlyphs = -1;
    private int unitsPerEm = -1;
    /**
     * WOFF TableDirectoryEntry
     */
    private List<TableDirectory> tableDirectories;

    private Map<String, AbstractWoffTable> tables = new HashMap<>(32);
    private final List<String> enabledGsubFeatures = new ArrayList<>();
    private Map<String, Integer> postScriptNames;

    private AbstractOTFDataStream data;

    private synchronized void readPostScriptNames() throws IOException {
        if (postScriptNames == null && getPostScript() != null) {
            String[] names = getPostScript().getGlyphNames();
            if (names != null) {
                postScriptNames = new HashMap<>(names.length);
                for (int i = 0; i < names.length; i++) {
                    postScriptNames.put(names[i], i);
                }
            } else {
                postScriptNames = new HashMap<>();
            }
        }
    }

    public WoffFont() {

    }

    public WoffFont(AbstractOTFDataStream stream) {
        this.data = stream;
    }

    public WoffFont parseWoffFontHeader(AbstractOTFDataStream data) throws IOException {
        header = new WoffHeader(data);
        header.read();
        return this;
    }

    @Override
    public void close() throws IOException {
        data.close();
    }

    void addTable(AbstractWoffTable table) {
        tables.put(table.getTag(), table);
    }

    public synchronized AbstractWoffTable getTable(String tag) throws IOException {
        AbstractWoffTable ttfTable = tables.get(tag);
        if (ttfTable != null) {
            readTable(ttfTable);
        }
        return ttfTable;
    }

    public void readTable(AbstractWoffTable table) throws IOException {
        long currentPosition = data.getCurrentPosition();
        data.seek(table.getOffset());
        table.read(this, data);
        // 将索引恢复到表头位置
        data.seek(currentPosition);
    }

    /**
     * maxp table data
     *
     * @return The maxp table.
     * @throws IOException if there was an error reading the table.
     */
    public MaximumProfileTable getMaximumProfile() throws IOException {
        return (MaximumProfileTable) getTable(MaximumProfileTable.TAG);
    }

    public WoffHeader getHeader() {
        return header;
    }

    public void setHeader(WoffHeader header) {
        this.header = header;
    }

    public List<TableDirectory> getTableDirectories() {
        return tableDirectories;
    }

    public void setTableDirectories(List<TableDirectory> tableDirectories) {
        this.tableDirectories = tableDirectories;
    }

    public Collection<AbstractWoffTable> getTables() {
        return tables.values();
    }

    public void setTables(Map<String, AbstractWoffTable> tables) {
        this.tables = tables;
    }

    public int getNumberOfGlyphs() throws IOException {
        if (numberOfGlyphs == -1) {
            MaximumProfileTable maximumProfile = getMaximumProfile();
            if (maximumProfile != null) {
                numberOfGlyphs = maximumProfile.getNumGlyphs();
            } else {
                // this should never happen
                numberOfGlyphs = 0;
            }
        }
        return numberOfGlyphs;
    }

    public void setNumberOfGlyphs(int numberOfGlyphs) {
        this.numberOfGlyphs = numberOfGlyphs;
    }

    /**
     * 获取header table
     *
     * @return The head table.
     * @throws IOException if there was an error reading the table.
     */
    public HeaderTable getHeaderTable() throws IOException {
        return (HeaderTable) getTable(HeaderTable.TAG);
    }

    /**
     * Get the loca table for this TTF.
     *
     * @return The loca table.
     * @throws IOException if there was an error reading the table.
     */
    public IndexToLocationTable getIndexToLocation() throws IOException {
        return (IndexToLocationTable) getTable(IndexToLocationTable.TAG);
    }

    public HorizontalMetricsTable getHorizontalMetrics() throws IOException {
        return (HorizontalMetricsTable) getTable(HorizontalMetricsTable.TAG);
    }

    public HorizontalHeaderTable getHorizontalHeader() throws IOException {
        return (HorizontalHeaderTable) getTable(HorizontalHeaderTable.TAG);
    }

    public PostScriptTable getPostScript() throws IOException {
        return (PostScriptTable) getTable(PostScriptTable.TAG);
    }

    public GlyphTable getGlyph() throws IOException {
        return (GlyphTable) getTable(GlyphTable.TAG);
    }

    public NamingTable getNaming() throws IOException {
        return (NamingTable) getTable(NamingTable.TAG);
    }

    public CmapTable getCmap() throws IOException {
        return (CmapTable) getTable(CmapTable.TAG);
    }

    public CmapLookup getUnicodeCmapLookup() throws IOException {
        return getUnicodeCmapLookup(true);
    }

    private CmapSubtable getUnicodeCmapImpl(boolean isStrict) throws IOException {
        CmapTable cmapTable = getCmap();
        if (cmapTable == null) {
            if (isStrict) {
                throw new IOException("The TrueType font " + getName() + " does not contain a 'cmap' table");
            } else {
                return null;
            }
        }

        CmapSubtable cmap = cmapTable.getSubtable(CmapTable.PLATFORM_UNICODE, CmapTable.ENCODING_UNICODE_2_0_FULL);
        if (cmap == null) {
            cmap = cmapTable.getSubtable(CmapTable.PLATFORM_WINDOWS, CmapTable.ENCODING_WIN_UNICODE_FULL);
        }
        if (cmap == null) {
            cmap = cmapTable.getSubtable(CmapTable.PLATFORM_UNICODE, CmapTable.ENCODING_UNICODE_2_0_BMP);
        }
        if (cmap == null) {
            cmap = cmapTable.getSubtable(CmapTable.PLATFORM_WINDOWS, CmapTable.ENCODING_WIN_UNICODE_BMP);
        }
        if (cmap == null) {
            // Microsoft's "Recommendations for OpenType Fonts" says that "Symbol" encoding
            // actually means "Unicode, non-standard character set"
            cmap = cmapTable.getSubtable(CmapTable.PLATFORM_WINDOWS, CmapTable.ENCODING_WIN_SYMBOL);
        }
        if (cmap == null) {
            if (isStrict) {
                throw new IOException("The TrueType font does not contain a Unicode cmap");
            } else if (cmapTable.getCmapSubtable().length > 0) {
                // fallback to the first cmap (may not be Unicode, so may produce poor results)
                cmap = cmapTable.getCmapSubtable()[0];
            }
        }
        return cmap;
    }

    public CmapLookup getUnicodeCmapLookup(boolean isStrict) throws IOException {
        CmapSubtable cmap = getUnicodeCmapImpl(isStrict);
        if (!enabledGsubFeatures.isEmpty()) {
            // GlyphSubstitutionTable table = getGsub();
            // if (table != null) {
            // return new SubstitutingCmapLookup(cmap, (GlyphSubstitutionTable) table,
            // Collections.unmodifiableList(enabledGsubFeatures));
            // }
        }
        return cmap;
    }

    public int getUnitsPerEm() throws IOException {
        if (unitsPerEm == -1) {
            HeaderTable header = getHeaderTable();
            if (header != null) {
                unitsPerEm = header.getUnitsPerEm();
            } else {
                unitsPerEm = 0;
            }
        }
        return unitsPerEm;
    }

    /**
     * <p>
     * Title: getName
     * </p>
     * <p>
     * Description:
     * </p>
     *
     * @return
     * @see com.wxd.woff.common.ITTFfontBox#getName()
     */
    @Override
    public String getName() throws IOException {
        NamingTable namingTable = getNaming();
        if (null != namingTable) {
            return namingTable.getPostScriptName();
        } else {
            return null;
        }
    }

    /**
     * <p>
     * Title: getFontBBox
     * </p>
     * <p>
     * Description:
     * </p>
     *
     * @return
     * @throws IOException
     * @see com.wxd.woff.common.ITTFfontBox#getFontBBox()
     */
    @Override
    public BoundingBox getFontBBox() throws IOException {
        short xMin = getHeaderTable().getxMin();
        short xMax = getHeaderTable().getxMax();
        short yMin = getHeaderTable().getyMin();
        short yMax = getHeaderTable().getyMax();
        int scale = (1000 / getUnitsPerEm());
        return new BoundingBox(xMin * scale, yMin * scale, xMax * scale, yMax * scale);
    }

    /**
     * <p>
     * Title: getFontMatrix
     * </p>
     * <p>
     * Description:
     * </p>
     *
     * @return
     * @throws IOException
     * @see com.wxd.woff.common.ITTFfontBox#getFontMatrix()
     */
    @Override
    public List<Number> getFontMatrix() throws IOException {
        float scale = 1000F / getUnitsPerEm();
        return Arrays.<Number>asList(0.001f * scale, 0, 0, 0.001f * scale, 0, 0);
    }

    public int nameToGID(String name) throws IOException {
        // look up in 'post' table
        readPostScriptNames();
        if (postScriptNames != null) {
            Integer gid = postScriptNames.get(name);
            if (gid != null && gid > 0 && gid < getMaximumProfile().getNumGlyphs()) {
                return gid;
            }
        }

        // look up in 'cmap'
        int uni = parseUniName(name);
        if (uni > -1) {
            CmapLookup cmap = getUnicodeCmapLookup(false);
            return cmap.getGlyphId(uni);
        }

        return 0;
    }

    private int parseUniName(String name) {
        if (name.startsWith("uni") && name.length() == 7) {
            int nameLength = name.length();
            StringBuilder uniStr = new StringBuilder();
            try {
                for (int chPos = 3; chPos + 4 <= nameLength; chPos += 4) {
                    int codePoint = Integer.parseInt(name.substring(chPos, chPos + 4), 16);
                    if (codePoint <= 0xD7FF || codePoint >= 0xE000) {
                        uniStr.append((char) codePoint);
                    }
                }
                String unicode = uniStr.toString();
                if (unicode.length() == 0) {
                    return -1;
                }
                return unicode.codePointAt(0);
            } catch (NumberFormatException e) {
                return -1;
            }
        }
        return -1;
    }

    /**
     * <p>
     * Title: getPath
     * </p>
     * <p>
     * Description:
     * </p>
     *
     * @param name
     * @return
     * @throws IOException
     * @see com.wxd.woff.common.ITTFfontBox#getPath(String)
     */
    @Override
    public GeneralPath getPath(String name) throws IOException {
        int gid = nameToGID(name);
        GlyphData glyph = getGlyph().getGlyph(gid);
        if (glyph == null) {
            return new GeneralPath();
        } else {
            return glyph.getPath();
        }
    }

    /**
     * <p>
     * Title: getWidth
     * </p>
     * <p>
     * Description:
     * </p>
     *
     * @param name
     * @return
     * @throws IOException
     * @see com.wxd.woff.common.ITTFfontBox#getWidth(String)
     */
    @Override
    public float getWidth(String name) throws IOException {
        Integer gid = nameToGID(name);
        return getAdvanceWidth(gid);
    }

    public int getAdvanceWidth(int gid) throws IOException {
        HorizontalMetricsTable hmtx = getHorizontalMetrics();
        if (hmtx != null) {
            return hmtx.getAdvanceWidth(gid);
        } else {
            return 250;
        }
    }

    /**
     * <p>
     * Title: hasGlyph
     * </p>
     * <p>
     * Description:
     * </p>
     *
     * @param name
     * @return
     * @throws IOException
     * @see com.wxd.woff.common.ITTFfontBox#hasGlyph(String)
     */
    @Override
    public boolean hasGlyph(String name) throws IOException {
        return nameToGID(name) != 0;
    }

    /**
     * 获取glyf表的点位坐标
     */
    public List<Point[]> getGlyfPoint() throws IOException {
        GlyphTable table = getGlyph();
        GlyphData[] datas = table.getGlyphs();
        List<Point[]> resultList = new ArrayList<>(datas.length);
        for (int i = 0; i < datas.length; i++) {
            GlyphData data = datas[i];
            // GlyphData 数组中第一个元素没有glyf数据
            if (null == data) {
                resultList.add(null);
            }
            GlyphDescription d = data.getDescription();
            resultList.add(d.getPoint());
        }
        return resultList;
    }

    /**
     * 映射cmap和glyf的关系
     */
    public List<TTFGraphicsData> getGraphicsData() throws IOException {
        GlyphTable table = getGlyph();
        GlyphData[] datas = table.getGlyphs();
        int length = datas.length;
        List<TTFGraphicsData> result = new ArrayList<>(length);
        CmapTable cmap = getCmap();
        Map<Integer, Integer> characterCodeToGlyphId = cmap.getWindowCharacterCodeToGlyphId();
        for (Map.Entry<Integer, Integer> entry : characterCodeToGlyphId.entrySet()) {
            // 字体unicode
            Integer key = entry.getKey();
            // 对应glfy表索引位置
            Integer value = entry.getValue();
            if (value > length) {
                LOGGER.error("cmap表索引长度大于glyf表数据长度  cmap :{} , glyf:{}", length, value);
                return null;
            }
            GlyphData data = datas[value - 1];
            TTFGraphicsData graphicsData = new TTFGraphicsData();
            if (null != data) {
                BoundingBox fondBox = data.getBoundingBox();
                Point[] points = data.getDescription().getPoint();
                graphicsData.setFontBox(fondBox);
                graphicsData.setPoints(points);
            }
            graphicsData.setTag(Integer.toHexString(key));
            result.add(graphicsData);
        }
        return result;
    }

    /**
     * 返回带有画图path的封装对象
     */
    public List<TTFGraphicsPathData> getGraphicsPathData() throws IOException {
        GlyphTable table = getGlyph();
        GlyphData[] datas = table.getGlyphs();
        int length = datas.length;
        TTFGraphicsPathData[] ttfGraphicsPathDatas = new TTFGraphicsPathData[length];
        CmapTable cmap = getCmap();
        Map<Integer, Integer> characterCodeToGlyphId = cmap.getWindowCharacterCodeToGlyphId();
        for (Map.Entry<Integer, Integer> entry : characterCodeToGlyphId.entrySet()) {
            // 字体unicode
            Integer key = entry.getKey();
            // 对应glfy表索引位置
            Integer value = entry.getValue();
            if (value > length) {
                LOGGER.error("cmap表索引长度大于glyf表数据长度  cmap :{} , glyf:{}", length, value);
                return null;
            }
            GlyphData data = datas[value];
            TTFGraphicsPathData graphicsData = new TTFGraphicsPathData();
            if (null != data) {
                BoundingBox fondBox = data.getBoundingBox();
                Point[] points = data.getDescription().getPoint();
                graphicsData.setFontBox(fondBox);
                graphicsData.setPoints(points);
                GeneralPath path = data.getPath();
                graphicsData.setPath(path);
            }
            graphicsData.setTag(Integer.toHexString(key));
            ttfGraphicsPathDatas[value - 1] = graphicsData;
        }
        List<TTFGraphicsPathData> result = new ArrayList<>(Arrays.asList(ttfGraphicsPathDatas));
        return result;
    }
}
